DataExplorer.Graph = (function() {

    var validX = ['Date', 'Number', 'Text'];
    var validY = ['Number'];
    var undefinable = "missing";
    var caret = "\u25BC";
    
    function constructor() {

        var cachedResultSet, xDate, xText, gt, xLabel, yLabel, margin, width, height;

        var options = {
            gridLines: true,
            lineDots: true,
            toolTips: true,
            interpolation: "linear",
            stackedPercent: false,
            color: d3.scale.category20()
        };
        
        var selections = {
            availableX: [],
            availableY: [],
            selectedX: 0,
            selectedY: [],
            yl: false,
            xl: false
        };
        
        gt = "line";
        
        function canGraph(resultSet) {
            try {
                if (!resultSet || resultSet.columns.length < 2 || resultSet.rows.length == 0) {
                    return false;
                }
                
                prepareData(resultSet);

                populateAvailableColumns();
                if (selections.availableX.length == 0 || selections.availableY.length == 0) {
                    return false;
                }

                // X's default choice *could* invalidate the graph if Y is left with no choice
                selections.selectedX = selections.availableX[0].index;
                var remainingY = selections.availableY.slice();
                findAndRemove(remainingY, 'index', selections.selectedX);
                if (!remainingY.length) {
                    return false;
                }

                // Select up to 10 available and valid options
                var preselect = 10;
                selections.selectedY = [];
                $.each(remainingY, function (i, y) {
                    if (i >= preselect) {
                        return false;
                    }
                    selections.selectedY.push(y.index);
                    return true;
                });

                return true;
            }
            catch (e) {
                //console.log(e);
                return false;
            }
        }
        
        function render(hash) {
            try {
                if (hash) {
                    // --> Make this an object (selections)
                    var graphType = hash.opts ? getParameterFromString("t", hash.opts) : null;
                    var xAxis = hash.opts ? parseInt(getParameterFromString("x", hash.opts)) : null;
                    var yAxis = hash.opts ? getParameterFromString("y", hash.opts) : null;
                    var yLog = hash.opts ? parseInt(getParameterFromString("yl", hash.opts)) : null;
                    var xLog = hash.opts ? parseInt(getParameterFromString("xl", hash.opts)) : null;
                    renderGraph(graphType, xAxis, yAxis, yLog, xLog);
                } else {
                    renderGraph();
                }
            } catch (e) {
                //console.log(e);
            }
        }

        function getType() {
            return gt;
        }
        function setType(v) {
            gt = v;
        }

        function getSelections() {
            return selections;
        }
        function setSelections(value) {
            selections = value;
        }
        
        function prepareData(resultSet) {
            // Cloning is necessary here to avoid mutating data between options
            cachedResultSet = $.extend(true, {}, resultSet);

            // Unpivot if the second column is text; following columns are y bands
            if (cachedResultSet.columns[1]["type"] == 'Text') {
                unpivot();
            }
            
            cleanseNullValues();
        }
        
        function populateAvailableColumns() {
            selections.availableX = [];
            selections.availableY = [];
            for (var i = 0; i < cachedResultSet.columns.length; i++) {
                var type = cachedResultSet.columns[i]["type"];
                var name = cachedResultSet.columns[i]["name"];
                if (name == "") {
                    name = "Column " + (i + 1);
                    cachedResultSet.columns[i]["name"] = name;
                }

                if ($.inArray(type, validX) > -1) {
                    selections.availableX.push({
                        index: i,
                        name: name,
                        type: type
                    });
                }
                if ($.inArray(type, validY) > -1) {
                    selections.availableY.push({
                        index: i,
                        name: name,
                        type: type
                    });
                }
            }
        }
        
        // This is not a robust solution yet, as it relies on three columns, but it does mimic the original
        function unpivot() {
            // --> Investigate use of "nest" function to clean up this implementation a bit
            var data = [];
            var columns = {};
            for (var i = 0; i < cachedResultSet.rows.length; i++) {
                var row = cachedResultSet.rows[i];
                var columnLabel = row[1];
                var columnName = "col_" + columnLabel;
                if (columns[columnName] === undefined) {
                    columns[columnName] = (data.push({ label: columnLabel, data: [] }) - 1);
                }
                data[columns[columnName]].data.push([cachedResultSet.rows[i][0], cachedResultSet.rows[i][2]]);
                cachedResultSet.columns.splice(1, 2);
            }
            cachedResultSet.rows = [];
            columns = {};
            var hash = {};
            $.each(data, function (ci, d) {
                if (columns[d.label] === undefined) {
                    columns[d.label] = cachedResultSet.columns.push({ name: d.label, type: "Number" });
                }
                $.each(d.data, function (ri, dr) {
                    if (!(dr[0] in hash)) {
                        hash[dr[0]] = [];
                    }
                    hash[dr[0]].push(dr[1]);
                });
            });
            var size = 0;
            $.each(hash, function (key, value) {
                var rv = [];
                rv.push(key);
                $.each(value, function (vi, vd) {
                    rv.push(vd);
                });
                if (rv.length > size) {
                    size = rv.length;
                }
                cachedResultSet.rows.push(rv);
            });
            $.each(cachedResultSet.rows, function (ri, rd) {
                while (rd.length < size) {
                    rd.push(undefinable);
                }
            });
        }
        
        function cleanseNullValues() {
            $.each(cachedResultSet.rows, function (ri, rd) {
                $.each(rd, function (ci, cd) {
                    if (cd == null || cd == "null" || cd == '') {
                        cachedResultSet.rows[ri][ci] = undefinable;
                    }
                });
            });
        }
        
        function updateGraphMeta() {

            margin = { top: 20, right: 20, bottom: 30, left: 50 };
            width = $("#graph").width() - margin.left - margin.right;
            height = $("#graph").height() - margin.top - margin.bottom;

            xDate = cachedResultSet.columns[selections.selectedX]["type"] == 'Date';
            xText = cachedResultSet.columns[selections.selectedX]["type"] == 'Text';

            xLabel = cachedResultSet.columns[selections.selectedX].name;
            yLabel = "";
            $.each(selections.selectedY, function (i, s) {
                yLabel += cachedResultSet.columns[s].name;
                if (i < selections.selectedY.length - 1) {
                    yLabel += ", ";
                }
            });

            $("#x-axis-options").html("X-axis: " + xLabel + " <span class=\"caret\">" + caret + "</span>");
            $("#y-axis-options").html("Y-axis: " + yLabel + " <span class=\"caret\">" + caret + "</span>");
        }

        // --> these options should be an object set elsewhere...
        function renderGraph(graphType, xAxis, yAxis, yLog, xLog) {

            if (!graphType && gt) {
                graphType = gt;
            }
            if (!graphType && !gt) {
                graphType = "line";
            }
            gt = graphType;

            if (yLog == undefined && selections.yl == true) {
                yLog = 1;
            }

            if (xLog == undefined && selections.xl == true) {
                xLog = 1;
            }

            setOptionsByHash(xAxis, yAxis, yLog, xLog);

            updateGraphMeta();

            selections.selectedY.sort(function (a, b) { return a - b; });

            var label = "Unknown type";
            switch (graphType) {
                case 'line':
                    renderLineGraph();
                    label = "Line";
                    break;
                case 'area':
                    renderAreaGraph();
                    label = "Area";
                    break;
                case 'bar':
                    renderBarGraph();
                    label = "Bar";
                    break;
                case 'stacked-area':
                    renderAreaGraph(true);
                    label = "Stacked Area";
                    break;
                case 'stacked-bar':
                    renderStackedBarGraph();
                    label = "Stacked Bar";
                    break;
            }

            $("#graph-type-options").html("Graph Type: " + label + " <span class=\"caret\">" + caret + "</span>");

            setHashByOptions();
        }

        function setHashByOptions() {
            var hash = parseHash();
            var full = hash.slug + "?t=" + gt + "&x=" + selections.selectedX + "&y=" + selections.selectedY.join(",") + "&yl=" + (selections.yl ? "1" : "0") + "&xl=" + (selections.xl ? "1" : "0");
            if (hash.slug == '#graph') {
                window.location.hash = full;
            }
            var permalink = $('#permalink a')[0];
            if (permalink) {
                permalink.href = permalink.href.replace(/#.*$/, '') + full;
            }
        }

        function setOptionsByHash(xAxis, yAxis, yLog, xLog) {

            // --> Need to decide what happens if the URL is hacked to graph the same column?
            var columns = cachedResultSet.columns.length;
            if (xAxis != null && xAxis >= 0 && xAxis <= (columns - 1)) {
                selections.selectedX = xAxis;
            }
            if (yAxis) {
                selections.selectedY = [];
                var y = yAxis.split(',');
                for (var i = 0; i < y.length; i++) {
                    y[i] = y[i].replace(/^\s*/, "").replace(/\s*$/, "");
                    selections.selectedY.push(parseInt(y[i]));
                }
            }
            selections.yl = yLog == 1;
            selections.xl = xLog == 1;
        }
        
        function getScalesForTypes() {

            var xScale = xText ? d3.scale.ordinal() :         // "Text"
                         xDate ? d3.time.scale.utc().nice() : // "Date"
                         selections.xl ?
                              d3.scale.log().clamp(true) :    // Log scaling
                              d3.scale.linear();              // "Number"

            var yScale = selections.yl ?
                            d3.scale.log().clamp(true) :      // Log scaling
                            d3.scale.linear();                // "Number"

            return { x: xScale, y: yScale };
        }

        function projectSeries() {
            var series = [];

            $.each(cachedResultSet.rows, function (i, d) {

                var xAxis = find(selections.availableX, 'index', selections.selectedX);
                var xCol = xAxis.name;
                var x = d[selections.selectedX];
                var cols = [];
                $.each(selections.selectedY, function (j, y) {
                    var axis = find(selections.availableY, 'index', y);
                    var col = axis.name;
                    var p = d[y];
                    cols.push("\"" + col + "\":\"" + p + "\"");
                });

                var json = "{ \"" + xCol + "\":\"" + x + "\", " + cols.join(", ") + "}";
                var row = $.parseJSON(json);
                series.push(row);
            });

            return series;
        }

        function renderLegend(graph, yLabels, topToBottom) {

            var legend = graph.selectAll(".legend")
                .data(topToBottom ? yLabels : yLabels.slice().reverse())
                .enter().append("g")
                .attr("class", "legend")
                .attr("transform", function (d, i) { return "translate(0," + i * 20 + ")"; });

            legend.append("rect")
                .attr("x", width - 18)
                .attr("width", 18)
                .attr("height", 18)
                .style("fill", options.color);

            legend.append("text")
                .attr("x", width - 24)
                .attr("y", 9)
                .attr("dy", ".35em")
                .style("text-anchor", "end")
                .text(function (d) { return d; });
        }
        
        function renderGraphContainer(gd) {

            d3.select("svg").remove();

            var graph = d3.select("#graph")
                .append("svg")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

            graph.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + height + ")")
                .call(gd.xAxis);

            graph.append("g")
                .attr("class", "y axis")
                .call(gd.yAxis);

            graph.append("text")
                .attr("x", "50%")
                .attr("y", height + margin.bottom - 2)
                .attr("style", "font-size:10px;font-weight:bold;")
                .style("text-anchor", "end")
                .text(xLabel);

            return graph;
        }

        function renderGridLines(graph, gd) {

            graph.append("g")
                .attr("class", "grid")
                .attr("transform", "translate(0," + height + ")")
                .call(gd.xAxis.tickSize(-height, 0, 0).tickFormat(""));

            graph.append("g")
                .attr("class", "grid")
                .attr("x2", width)
                .call(gd.yAxis.tickSize(-width, 0, 0).tickFormat(""));
        }

        function renderLineGraph() {

            var data = projectSeries();
            var gd = buildLineData(data);
            var graph = renderGraphContainer(gd);

            if (options.gridLines) {
                renderGridLines(graph, gd);
            }

            renderLines(graph, gd);

            renderLegend(graph, gd.yLabels);
        }

        function renderAreaGraph(stacked) {

            var data = projectSeries();
            var gd = buildLineData(data, stacked);
            var graph = renderGraphContainer(gd);

            if (options.gridLines && !options.stackedPercent) {
                renderGridLines(graph, gd);
            }

            var area;
            if (stacked) {
                area = d3.svg.area()
                    .x(function (d) { return gd.x(d.x); })
                    .y0(function (d) { return gd.y(d.y0); })
                    .y1(function (d) { return gd.y(d.y0 + d.y); });
            } else {
                area = d3.svg.area()
                    .x(function (d) { return gd.x(d.x); })
                    .y0(height)
                    .y1(function (d) { return gd.y(d.y); });
            }
            area.interpolate(options.interpolation);
            area.defined(function (d) { return !isNaN(d.y) && (xText || !isNaN(d.x)); });

            var areas = graph.selectAll(".areas")
                .data(gd.pairs)
                .enter().append("g").attr("class", "areas");

            areas.append("path")
                .attr("class", "area")
                .attr("d", function (d) { return area(d.values); })
                .style("fill", function (d) { return options.color(d.name); })
                .style("fill-opacity", "0.5");

            if (stacked) {
                renderLines(graph, gd, function (d) { return gd.y(d.y + d.y0); });
            } else {
                renderLines(graph, gd);
            }

            renderLegend(graph, gd.yLabels);
        }

        function renderLines(graph, gd, yFunc) {

            if (!yFunc) {
                yFunc = function (d) { return gd.y(d.y); };
            }

            var line = d3.svg.line()
               .interpolate(options.interpolation)
               .x(function (d) { return gd.x(d.x); })
               .y(yFunc)
               .defined(function (d) { return !isNaN(d.y) && (xText || !isNaN(d.x)); });

            var lines = graph.selectAll(".lines")
                .data(gd.pairs)
                .enter().append("g").attr("class", "lines");

            lines.append("path")
                .attr("class", "line")
                .attr("d", function (d) {
                    return line(d.values);
                })
                .style("stroke", function (d) { return options.color(d.name); });

            if (options.lineDots) {
                renderDots(lines, gd, yFunc);
            }
        }

        function renderDots(el, gd, yFunc) {
            var dots = el.selectAll("circle")
                .data(function (d) {
                    var clean = d.values.filter(function (d1) {
                        return !isNaN(d1.y) && (xText || !isNaN(d1.x));
                    });
                    return clean;
                })
                .enter().append("circle")
                .attr("r", 2.5)
                .attr("cx", function (d) { return gd.x(d.x); })
                .attr("cy", yFunc)
                .style("stroke", function (d) { return options.color(d.name); })
                .style("fill", "white");

            if (options.toolTips) {
                renderToolTips(dots, function (d) {
                    var x = d.x;
                    var y = d.y;
                    if (xDate) {
                        x = getTimeFormat(x);
                    }
                    var text = "x: " + x + "<br/>" + "y: " + y + "<br/>" + "(" + d.name + ")";
                    return text;
                });
            }
        }

        function renderBarGraph() {

            var data = projectSeries();
            var gd = buildBarData(data);
            var graph = renderGraphContainer(gd);

            var bars = graph.selectAll(".bars")
                .data(data)
                .enter().append("g")
                .attr("class", "g")
                .attr("transform", function (d) {
                    var x0 = gd.x0(d[xLabel]);
                    return "translate(" + x0 + ", 0)";
                });

            var rects = bars.selectAll("rect")
                .data(function (d) { return d.yValues; })
                .enter().append("rect")
                .attr("width", gd.x1.rangeBand())
                .attr("class", function (d) {
                    return d.value < 0 ? "bar negative" : "bar positive";
                })
                .attr("x", function (d) {
                    var x1 = gd.x1(d.name);
                    return x1;
                })
                .attr("y", function (d) {
                    var y = gd.y(Math.max(0, d.value));
                    return y;
                })
                .attr("height", function (d) {
                    var y = Math.abs(gd.y(d.value) - gd.y(0));
                    return y;
                })
                .style("fill", function (d) { return options.color(d.name); });

            if (options.toolTips) {
                renderToolTips(rects, function (d) {
                    var text = d.name + "<br/>" + d.value + "<br/>" + "(" + d.x + ")";
                    return text;
                });
            }

            renderLegend(graph, gd.yLabels);
        }

        function renderStackedBarGraph() {

            var data = projectSeries();
            var gd = buildStackedBarData(data);
            var graph = renderGraphContainer(gd);

            var bars = graph.selectAll(".state")
                .data(data)
                .enter().append("g")
                .attr("class", "g")
                .attr("transform", function (d) { return "translate(" + gd.x(d[xLabel]) + ",0)"; });

            var rects = bars.selectAll("rect")
                .data(function (d) { return d.yValues; })
                .enter().append("rect")
                .attr("width", gd.x.rangeBand())
                .attr("y", function (d) {
                    var y = gd.y(d.y1);
                    return y;
                })
                .attr("height", function (d) { return gd.y(d.y0) - gd.y(d.y1); })
                .style("fill", function (d) { return options.color(d.name); });

            if (options.toolTips) {
                renderToolTips(rects, function (d) {
                    var text = d.name + "<br/>" + d.y1 + "<br/>" + "(" + d.x + ")";
                    return text;
                });
            }

            renderLegend(graph, gd.yLabels);
        }

        function renderToolTips(el, textFunction) {
            var div = d3.select("body").append("div")
                    .attr("class", "tooltip")
                    .style("opacity", 0);

            el.on("mouseover", function () {
                div.style("cursor", "pointer");
                div.transition().duration(50).style("opacity", 1);
            });

            el.on("mouseout", function () {
                div.style("cursor", "default");
                div.transition().duration(250).style("opacity", 0);
            });

            el.on("mousemove", function (d) {
                div.html(textFunction(d));
                var w = parseFloat(div.style("width").replace("px", ""));
                var h = parseFloat(div.style("height").replace("px", ""));

                var x = d3.event.pageX;
                var y = d3.event.pageY;

                var left = x - Math.round(w / 2) - 10;
                var top = (y - (h * 2));

                div.style("left", left + "px").style("top", top + "px");
            });
        }

        function buildLineData(data, stacked) {

            var scales = getScalesForTypes();

            xLabel = selections.availableX[selections.selectedX].name;
            var yLabels = d3.keys(data[0]).filter(function (key) { return key !== xLabel; });
            data.forEach(function (d) {
                d.xValue = xDate ? new Date(+d[xLabel]) : xText ? d[xLabel] : +d[xLabel],
                d.yValues = yLabels.map(function (name) {
                    return { name: name, value: +d[name] };
                });
            });

            var x;
            if (xText) {
                x = scales.x;
                x.domain(data.map(function (d) { return d[xLabel]; })).rangePoints([0, width], 0);
            } else {
                x = scales.x.range([0, width]);
                x.domain(d3.extent(data, function (d) { return d.xValue; }));
            }

            var xAxis = d3.svg.axis().scale(x).orient("bottom");
            if (!xText && !xDate) {
                xAxis.tickFormat(d3.format(".2s"));
            }
           
            var y = scales.y.range([height, 0]);
            var yAxis = d3.svg.axis().scale(y).orient("left");

            if (stacked && options.stackedPercent) {
                yAxis.tickFormat(d3.format(".0%"));
            } else {
                yAxis.tickFormat(d3.format(".2s"));
            }

            var pairs;
            if (stacked) {
                var stack = d3.layout.stack().values(function (d) { return d.values; });
                pairs = stack(yLabels.map(function (name) {
                    return {
                        name: name,
                        values: data.map(function (d) {
                            var value;
                            if (options.stackedPercent) {
                                var values = d.yValues.map(function (d1) { return d1.value; });
                                value = d[name] / d3.sum(values);
                            } else {
                                value = +d[name];
                            }
                            return { name: name, x: d.xValue, y: value };
                        })
                    };
                }));
                if (options.stackedPercent) {
                    y.domain([selections.yl ? 1 : 0, 1]);
                } else {
                    // Complete y range is the max of the sums of each series
                    var maxOfSums = 0;
                    $.each(data, function (i, d) {
                        var values = d.yValues.map(function (d1) { return d1.value; });
                        var sum = d3.sum(values);
                        if (sum > maxOfSums) {
                            maxOfSums = sum;
                        }
                    });

                    var minOfSums = 9007199254740992;
                    $.each(data, function (i, d) {
                        var values = d.yValues.map(function (d1) { return d1.value; });
                        var sum = d3.sum(values);
                        if (sum < minOfSums) {
                            minOfSums = sum;
                        }
                    });

                    if (selections.yl) {
                        y.domain([minOfSums > 0 ? minOfSums : 1, maxOfSums]);
                    } else {
                        /* Using this approach cuts off values from the graph since it is 0-based                
                        y.domain([minOfSums, maxOfSums]);
                        */
                        y.domain([0, maxOfSums]); // <-- This needs to support negative values!
                    }
                }
            } else {
                pairs = yLabels.map(function (name) {
                    return {
                        name: name,
                        values: data.map(function (d) {
                            return { name: name, x: d.xValue, y: d[name], };
                        })
                    };
                });

                var ymax = d3.max(data, function (d) { return d3.max(d.yValues, function (d1) { return d1.value; }); });
                if (selections.yl && (gt !== "line" && gt !== "area")) {
                    y.domain([1, ymax]).nice();
                } else {
                    var ymin = d3.min(data, function (d) { return d3.min(d.yValues, function (d1) { return d1.value; }); });
                    if (selections.yl) {
                        ymin = ymin > 0 ? ymin : 1;
                    }
                    y.domain([ymin, ymax]).nice();
                }
            }

            // Sort the pairs so the lines and paths are contiguous
            $.each(pairs, function (i, d) {
                d.values.sort(function (a, b) {
                    return d3.ascending(+a.x, +b.x);
                });
            });

            return {
                xAxis: xAxis,
                yAxis: yAxis,
                x: x, y: y,
                xLabel: xLabel,
                yLabels: yLabels,
                pairs: pairs
            };
        }

        function buildBarData(data) {

            var scales = getScalesForTypes();
            scales.x = d3.scale.ordinal();

            xLabel = selections.availableX[selections.selectedX].name;
            var yLabels = d3.keys(data[0]).filter(function (key) { return key !== xLabel; });
            data.forEach(function (d) {
                var x = xDate ? new Date(+d[xLabel]) : xText ? d[xLabel] : +d[xLabel];
                d.xValue = x,
                d.yValues = yLabels.map(function (name) {
                    var v = +d[name];
                    if (isNaN(v)) {
                        v = 0;
                    }
                    return { name: name, value: v, x: x };
                });
            });

            var x0 = scales.x;
            var xAxis = d3.svg.axis().scale(x0).orient("bottom");
            
            if (xDate) {
                var first = d3.min(data, function (d) { return d.xValue; });
                var last = d3.max(data, function (d) { return d.xValue; });
                x0.domain(d3.time.months(first, last));
                xAxis.tickFormat(getTimeFormat());
            } else {
                var clean = data.filter(function (d) { return xText || !isNaN(d.xValue); });
                var xd = clean.map(function (d) { return d.xValue; });
                x0.domain(xd);
                xAxis = d3.svg.axis().scale(x0).orient("bottom");
            }
            x0.rangeRoundBands([0, width], .1);

            if (!xText && !xDate) {
                xAxis.tickFormat(d3.format(".2s"));
            }

            var y = scales.y.range([height, 0]).nice();
            var yAxis = d3.svg.axis().scale(y).orient("left");
            yAxis.tickFormat(d3.format(".2s"));

            var ymin = d3.min(data, function (d) { return d3.min(d.yValues, function (d1) { return d1.value; }); });
            var ymax = d3.max(data, function (d) { return d3.max(d.yValues, function (d1) { return d1.value; }); });

            if (selections.yl) {
                y.domain([ymin > 0 ? ymin : 1, ymax]).nice();
            } else {
                y.domain([ymin < 0 ? ymin : 0, ymax]).nice();
            }

            var x1 = d3.scale.ordinal().domain(yLabels).rangeRoundBands([0, x0.rangeBand()]);

            return {
                xAxis: xAxis,
                yAxis: yAxis,
                x0: x0, x1: x1, y: y,
                xLabel: xLabel,
                yLabels: yLabels,
            };
        }

        function buildStackedBarData(data) {
            var scales = getScalesForTypes();
            scales.x = d3.scale.ordinal();

            xLabel = selections.availableX[selections.selectedX].name;
            var yLabels = d3.keys(data[0]).filter(function (key) { return key !== xLabel; });
            data.forEach(function (d) {
                var xv = xDate ? new Date(+d[xLabel]) : xText ? d[xLabel] : +d[xLabel];
                var y0 = 0;
                d.xValue = xv;
                d.yValues = yLabels.map(function (name) {
                    var v = +d[name];
                    if (isNaN(v)) {
                        v = 0;
                    }
                    return { name: name, y0: y0, y1: y0 += v, x: xv };
                });
                d.total = d.yValues[d.yValues.length - 1].y1;
            });

            var x = scales.x;
            var xAxis = d3.svg.axis().scale(x).orient("bottom");
            if (xDate) {
                var first = d3.min(data, function (d) { return d.xValue; });
                var last = d3.max(data, function (d) { return d.xValue; });
                x.domain(d3.time.months(first, last));
                xAxis.tickFormat(getTimeFormat());
            } else {
                var clean = data.filter(function (d) { return xText || !isNaN(d.xValue); });
                x.domain(clean.map(function (d) { return d.xValue; }));
            }
            x.rangeRoundBands([0, width], .1);

            if (!xText && !xDate) {
                xAxis.tickFormat(d3.format(".2s"));
            }

            var y = scales.y.rangeRound([height, 0]).nice();
            var yAxis = d3.svg.axis().scale(y).orient("left");
            yAxis.tickFormat(d3.format(".2s"));

            // Non-log is 0 based, log can't use a 0 base
            var ymax = d3.max(data, function (d) { return d.total; });
            //var ymin = d3.min(data, function (d) { return d.total; });
            if (selections.yl) {
                /*  Using this approach cuts off values from the graph since it is 0-based
                var ymin = d3.min(data, function (d) { return d.total; });
                */
                y.domain([1, ymax]).nice();
            } else {
                y.domain([0, ymax]).nice(); // <-- This needs to accept negative values! (ref: http://bl.ocks.org/2975320)
            }

            return {
                xAxis: xAxis,
                yAxis: yAxis,
                x: x, y: y,
                xLabel: xLabel,
                yLabels: yLabels,
            };
        }
        
        // Necessary for generating the same time scale as the linear default, but for ordinal labels and tooltips
        var defaultTimeFormat;
        function getTimeFormat() {
            if (defaultTimeFormat === undefined) {
                defaultTimeFormat = timeFormat([
                    [d3.time.format("%Y"), function () { return true; }],
                    [d3.time.format("%B"), function (d) { return d.getMonth(); }],
                    [d3.time.format("%b %d"), function (d) { return d.getDate() != 1; }],
                    [d3.time.format("%a %d"), function (d) { return d.getDay() && d.getDate() != 1; }],
                    [d3.time.format("%I %p"), function (d) { return d.getHours(); }],
                    [d3.time.format("%I:%M"), function (d) { return d.getMinutes(); }],
                    [d3.time.format(":%S"), function (d) { return d.getSeconds(); }],
                    [d3.time.format(".%L"), function (d) { return d.getMilliseconds(); }]
                ]);
            }
            return defaultTimeFormat;
        }

        function timeFormat(formats) {
            return function (date) {
                var i = formats.length - 1, f = formats[i];
                while (!f[1](date)) f = formats[--i];
                return f[0](date);
            };
        }
        
        return { 
            'canGraph': canGraph,
            'render': render,
            'getType': getType,
            'setType': setType,
            'getSelections': getSelections,
            'setSelections': setSelections
        };
    }
    return constructor;
})();

DataExplorer.ready(function () {

    DataExplorer.graph = new DataExplorer.Graph();
    var graph = DataExplorer.graph;
    
    var opt = 0;
    $("#graph-type-options").click(function (e) {
        e.preventDefault();
        closeOptionPanel();

        if (opt == 1) {
            opt = 0;
            return;
        }

        var gt = graph.getType();

        var ui = "<div>" +
            "<input id=\"rb-line\" type=\"radio\" name=\"graph-type\" value=\"line\" " + (gt == "line" ? "checked" : "") + "/><label for=\"rb-line\"> Line</label><br/>" +
            "<input id=\"rb-area\" type=\"radio\" name=\"graph-type\" value=\"area\" " + (gt == "area" ? "checked" : "") + "/><label for=\"rb-area\"> Area</label><br/>" +
            "<input id=\"rb-stacked-area\" type=\"radio\" name=\"graph-type\" value=\"stacked-area\" " + (gt == "stacked-area" ? "checked" : "") + "/><label for=\"rb-stacked-area\"> Stacked Area</label><br/>" +
            "<input id=\"rb-bar\" type=\"radio\" name=\"graph-type\" value=\"bar\" " + (gt == "bar" ? "checked" : "") + "/><label for=\"rb-bar\"> Bar</label><br/>" +
            "<input id=\"rb-stacked-bar\" type=\"radio\" name=\"graph-type\" value=\"stacked-bar\" " + (gt == "stacked-bar" ? "checked" : "") + "/><label for=\"rb-stacked-bar\"> Stacked Bar</label>" +
            "</div>";

        $("#option-panel").empty();
        $(ui).appendTo("#option-panel");

        $("#rb-line").click(function () {
            graph.setType("line");
            graph.render();
        });
        $("#rb-area").click(function () {
            graph.setType("area");
            graph.render();
        });
        $("#rb-stacked-area").click(function() {
            graph.setType("stacked-area");
            graph.render();
        });
        $("#rb-bar").click(function () {
            graph.setType("bar");
            graph.render();
        });
        $("#rb-stacked-bar").click(function () {
            graph.setType("stacked-bar");
            graph.render();
        });

        opt = 1;
        openOptionPanel(this);
    });
    
    $("#x-axis-options").click(function (e) {
        e.preventDefault();
        closeOptionPanel();

        if (opt == 2) {
            opt = 0;
            return;
        }
        
        selectorPanel("x");
        opt = 2;
        openOptionPanel(this);
    });
    
    $("#y-axis-options").click(function (e) {
        e.preventDefault();
        closeOptionPanel();

        if (opt == 3) {
            opt = 0;
            return;
        }

        selectorPanel("y");
        opt = 3;
        openOptionPanel(this);
    });
    
    function closeOptionPanel() {
        $(".option-block a").removeClass("youarehere");
        if ($("#option-panel").is(":visible")) {
            $("#option-panel").css('z-index', '0').hide();
        }
    }

    function openOptionPanel(el) {
        $(el).addClass("youarehere");
        var left = 0;
        switch (opt) {
            case 2:
                left = $(el).prev().width() + 34;
                break;
            case 3:
                left = $(el).prev().width() + $(el).prev().prev().width() + 66;
                break;
        }

        // Account for width of log scaling option
        var width = $(el).width();
        if ((opt == 2 || opt == 3) && width < 160) {
            width = 160;
        }

        $("#option-panel")
            .css('left', left + 'px')
            .css('width', width)
            .css('z-index', '9999').show();
    }

    function selectorPanel(axis) {

        var isX = axis == "x";

        var selections = graph.getSelections();
        
        var available = isX ? selections.availableX : selections.availableY;
        var selected = [];
        if (isX) {
            selected.push(selections.selectedX);
        } else {
            selected = selections.selectedY;
        }

        var hr = "<div class=\"separator\"></div>";

        $("#option-panel").empty();
        var ui = "";
        for (var i = 0; i < available.length; i++) {
            var ii = available[i].index;
            var name = available[i].name;
            var checked = $.inArray(ii, selected) > -1 ? "checked" : "";

            var disabled = "";
            var disabledCss = "";

            // Don't provide an option for X that is also Y's only valid option
            if (isX && selections.availableY.length == 1 && ii == selections.availableY[0].index) {
                disabled = "disabled";
                disabledCss = "color:gray";
            }

            // Don't provide an option in Y that is also X's current option
            if (!isX && selections.selectedX == ii) {
                disabled = "disabled";
                disabledCss = "color:gray";
            }

            var type = isX ? "radio" : "checkbox";
            ui += "<input id=\"rb-" + axis + "-" + ii + "\" type=\"" + type + "\" name=\"x-axis\" value=\"" + name + "\" " + checked + " " + disabled + "/><label for=\"rb-x-" + ii + "\" style=\"" + disabledCss + "\"> " + name + "</label><br/>";
        }

        var isBar = (graph.getType() == "bar" || graph.getType() == "stacked-bar");

        if (isX && !isBar && selections.availableX[selections.selectedX].type == "Number") {
            ui += "<div class=\" xlog\">" + hr;
            checked = selections.xl == true ? "checked" : "";
            ui += "<input type=\"checkbox\" id=\"x-log\" " + checked + "><label for=\"x-log\"> Use log scaling</label></input></div>";
        }

        if (!isX) {
            ui += hr;
            checked = selections.yl == true ? "checked" : "";
            ui += "<input type=\"checkbox\" id=\"y-log\" " + checked + "><label for=\"y-log\"> Use log scaling</label></input>";
        }

        $("#option-panel").empty();
        $(ui).appendTo("#option-panel");

        if (!isX) {
            $("#y-log").click(function () {
                var s = graph.getSelections();
                s.yl = ($(this).attr("checked") == 'checked');
                
                graph.setSelections(s);
                graph.render();
            });
        } else {
            $("#x-log").click(function () {
                var s = graph.getSelections();
                s.xl = $(this).attr("checked") == 'checked';
                graph.setSelections(s);
                graph.render();
            });
        }

        for (var j = 0; j < available.length; j++) {
            var ji = available[j].index;
            $("#rb-" + axis + "-" + ji).click(function () {
                var target = parseInt(this.id.substring(5));
                switch (axis) {
                    case "x":
                        selections.selectedX = target;
                        var indexInY = $.inArray(selections.selectedX, selections.selectedY);
                        if (indexInY > -1) {
                            var remainingY = selections.selectedY.slice();
                            remainingY.splice(indexInY, 1);
                            if (remainingY.length == 0) {
                                remainingY = selections.availableY.slice();
                                findAndRemove(remainingY, 'index', selections.selectedX);
                                selections.selectedY = [];
                                selections.selectedY.push(remainingY[0].index);
                            }
                            else {
                                selections.selectedY = remainingY;
                            }
                        }
                        break;
                    case "y":
                        var index = $.inArray(target, selections.selectedY);
                        if (index > -1) {
                            if (selections.selectedY.length > 1) {
                                selections.selectedY.splice(index, 1);
                            } else {
                                $(this).attr('checked', true);
                            }
                        } else {
                            selections.selectedY.push(target);
                            if (target == selections.selectedX) {
                                var remainingX = selections.availableX.slice();
                                findAndRemove(remainingX, 'index', selections.selectedY);
                                selections.selectedX = remainingX[0].index;
                            }
                        }
                        break;
                }

                if (isX && selections.availableX[selections.selectedX].type == "Number" && !isBar) {
                    if ($('.xlog').length == 0) {
                        ui = "<div class=\"xlog\">" + hr;
                        checked = selections.xl == true ? "checked" : "";
                        ui += "<input type=\"checkbox\" id=\"x-log\" " + checked + "><label for=\"x-log\"> Use log scaling</label></input></div>";
                        $(ui).appendTo("#option-panel");
                        $("#x-log").click(function () {
                            var s = graph.getSelections();
                            s.xl = $(this).attr("checked") == 'checked';
                            graph.setSelections(s);
                            graph.render();
                        });
                    }
                } else {
                    $('.xlog').remove();
                }

                graph.setSelections(selections);
                graph.render();
            });
        }
    }
});

// SO 6310623
function findAndRemove(array, property, value) {
    $.each(array, function (index, result) {
        if(result[property] == value) {
            array.splice(index, 1);
            return false;
        }
        return true;
    });
}

function findAll(array, property, value) {
    var elem = [];
    $.each(array, function (index, result) {
        if (result[property] == value) {
            elem.push(result);
            return false;
        }
        return true;
    });
    return elem;
}

function find(array, property, value) {
    var elem;
    $.each(array, function (index, result) {
        if (result[property] == value) {
            elem = result;
            return false;
        }
        return true;
    });
    return elem;
}

function findIndex(array, property, value) {
    var elem = -1;
    $.each(array, function (index, result) {
        if (result[property] == value) {
            elem = index;
            return false;
        }
        return true;
    });
    return elem;
}

// Compatibility in IE < 9 for filtering arrays
// See: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
    Array.prototype.filter = function (fun /*, thisp */) {
        "use strict";

        if (this == null)
            throw new TypeError();

        var t = Object(this);
        var len = t.length >>> 0;
        if (typeof fun != "function")
            throw new TypeError();

        var res = [];
        var thisp = arguments[1];
        for (var i = 0; i < len; i++) {
            if (i in t) {
                var val = t[i]; // in case fun mutates this
                if (fun.call(thisp, val, i, t))
                    res.push(val);
            }
        }

        return res;
    };
}